 <html>
   <head>
      <title>Illustration for DiagramDnD</title>

      <style type="text/css">
       canvas {
          border: 1px solid black;
       }
      </style>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.js"></script>
      <script>module = { };</script>
      <script src="https://cdn.jsdelivr.net/npm/three-trackballcontrols@0.0.7/index.js"></script>
      <script>THREE.TrackballControls = module.exports;</script>
      <script src="../lib/THREE.MeshLine.js"></script>
      <script>
       window.addEventListener('load', load, {once: true});

       var scene,
           camera,
           renderer,
           camControls;

       var WIDTH = 1500,
           HEIGHT = 1000;

       function load() {
          let obj;
          
          // Scene
          scene = new THREE.Scene();

          // Renderer
          renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true, antialias: true});
          renderer.setSize(WIDTH, HEIGHT);
          renderer.setClearColor('white', 1.0);
          $('#graphic').append(renderer.domElement);

          // Camera
          camera = new THREE.PerspectiveCamera(30, WIDTH/HEIGHT, 0.1, 2000);
          camera.position.set(-3.69, 1.10, 2.67);
          camera.up.set(0.40, 0.96, 0.03);
          camera.lookAt(new THREE.Vector3(0, 0, 0));

          // Light
          scene.add(new THREE.AmbientLight());

          const MID = new THREE.Vector3(0, 0, 0),
                START = new THREE.Vector3(-1, 0, 0),
                END = new THREE.Vector3(1, 0, 0),
                Y0 = new THREE.Vector3(0, 0.3, 0),
                YMAX = new THREE.Vector3(0, 0.4, 0),
                PICK = new THREE.Vector3(-0.4, y(-0.4, YMAX.y), 0),
                PROJ = new THREE.Vector3(-0.4, 0, 0),
                CENTER = new THREE.Vector3(0, -1, 0),
                CAMERA = new THREE.Vector3(0, -0.35, 1),
                ORIGIN = new THREE.Vector3(0, -0.35, -0.2),
                RAY = new THREE.Vector3().addScaledVector(PICK, 0.4).addScaledVector(CAMERA, 0.6),
                PAST = new THREE.Vector3().addScaledVector(PICK, 1.2).addScaledVector(CAMERA, -0.2),
                U = new THREE.Vector3(0.2, 0, 0).add(START),
                V = new THREE.Vector3(0, 0.2, 0).add(START);

          scene.add(makeSphere(START, 0.02));
          scene.add(makeSphere(START, 0.06, {color: '#8C8C8C', opacity: 0.7, transparent: true}));
          scene.add(makeSphere(END, 0.02));
          scene.add(makeSphere(END, 0.06, {color: '#8C8C8C', opacity: 0.7, transparent: true}));
          scene.add(makeSphere(PICK, 0.02));
          scene.add(makeSphere(CENTER, 0.02));

          scene.add(makeMeshLine([CENTER, START], 'black', true));
          scene.add(makeArrowhead([CENTER, START], 'black'));
          scene.add(makeMeshLine([END, CENTER], 'black', true));
          scene.add(makeArrowhead([END, CENTER], 'black'));
          scene.add(makeMeshLine([START, END], 'black'));
          scene.add(makeMeshLine([ORIGIN, CAMERA], 'black'));
          scene.add(makeArrowhead([ORIGIN, CAMERA], 'black'));
          scene.add(makeMeshLine([CAMERA, PAST], 'black'));
          scene.add(makeArrowhead([CAMERA, RAY], 'black'));

          scene.add(makeMeshLine([START, PICK], 'black'));
          scene.add(makeArrowhead([START, PICK], 'black'));
          scene.add(makeMeshLine([PICK, END], 'black'));
          scene.add(makeArrowhead([PICK, END], 'black'));
          scene.add(makeLine([PICK, PROJ], 'black'));
          scene.add(makeLine([MID, YMAX], 'black'));
          scene.add(obj = makeMeshLine([START, U], 'red'));
          obj.renderOrder = 10;
          scene.add(obj = makeArrowhead([START, U], 'red'));
          obj.renderOrder = 10;
          scene.add(obj = makeMeshLine([START, V], 'red'));
          obj.renderOrder = 10;
          scene.add(obj = makeArrowhead([START, V], 'red'));
          obj.renderOrder = 10;

          scene.add(makeCurve([START, YMAX, END], 'black', true));
          scene.add(makeCurve([START, Y0, END], 'gray', true));

          scene.add(makeLabel(new THREE.Vector3(0, -0.04, 0).add(U), 'U', 0.03, 'red', 20));
          scene.add(makeLabel(new THREE.Vector3(0, -0.04, 0).add(V), 'V', -0.015, 'red', 20));
          scene.add(makeLabel(new THREE.Vector3(-0.1, 0.07, 0).add(PAST), 'pick_line', -0.05, 'black', 20));
          scene.add(makeLabel(new THREE.Vector3(0, 0.01, 0).add(RAY), 'raycaster.ray.direction', -0.1, 'black', 20));
          scene.add(makeLabel(new THREE.Vector3(0, -0.05, 0).add(CAMERA), 'raycaster.ray.origin', 0.13, 'black', 20));
          scene.add(makeLabel(new THREE.Vector3(0, 0.03, 0).add(CAMERA), 'Camera', -0.03, 'black', 30));
          scene.add(makeLabel(new THREE.Vector3(0, 0, 0).add(ORIGIN), 'Origin', 0.25, 'black', 30));
          scene.add(makeLabel(new THREE.Vector3(0, -0.01, 0).add(PICK), 'pick', 0.16, 'black', 24));
          scene.add(makeLabel(new THREE.Vector3(0, 0, 0).add(START), 'start', 0.25, 'black', 30));
          scene.add(makeLabel(new THREE.Vector3(0, 0.05, 0).add(END), 'end', -0.1, 'black', 40));
          scene.add(makeLabel(new THREE.Vector3(0, -0.1, 0).add(CENTER), 'center', 0.1, 'black', 36));
          scene.add(makeLabel(new THREE.Vector3(0.0, y(0.0, YMAX.y) + 0.09, 0), 'New Arc', -0.05, 'black', 36));
          scene.add(makeLabel(new THREE.Vector3(0.4, y(0.4, Y0.y) + 0.04, 0), 'Old Arc', -0.1, 'gray', 36));

          scene.add(obj =
             makeText(new THREE.Vector3(-0.12, 0, 0).addScaledVector(PROJ, 0.6).addScaledVector(PICK, 0.4), 'pick_v', 'black', 20));
          obj.quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), -Math.PI/2);
          scene.add(
             makeText(new THREE.Vector3(0, 0.08, 0).addScaledVector(START, 0.55).addScaledVector(PROJ, 0.45), 'pick_u', 'black', 20));
          scene.add(
             makeText(new THREE.Vector3(-0.15, -0.01, 0).add(MID), 'separation', 'black', 20));
          scene.add(obj =
             makeText(new THREE.Vector3(-0.12, -0.05, 0).addScaledVector(MID, 0.5).addScaledVector(YMAX, 0.5), 'offset', 'black', 20));
          obj.quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), -Math.PI/2);

          // Trackball controls
          camControls = new THREE.TrackballControls(camera, $('graphic')[0]);

          // showAxes();
          render();
       }

       function makeText(location, text, color = 'black', font_size = 20) {
          const LABEL_WIDTH = 128,
                LABEL_HEIGHT = 32,
                scale = 0.0025;

          const canvas = $(`<canvas width="${LABEL_WIDTH}" height="${LABEL_HEIGHT}"></canvas>`)[0];
          const context = canvas.getContext('2d');

          context.font = `italic ${font_size}pt Arial`;
          const text_width = context.measureText(text).width;
          const margin = 5;

          //context.fillStyle = 'rgba(255, 0, 255, 0.2)';
          //context.fillRect(0, 0, canvas.width, canvas.height);

          context.textBaseline = 'middle';
          context.fillStyle = color;
          context.fillText(text, 0, LABEL_HEIGHT/2);

          const texture = new THREE.CanvasTexture(canvas);
          const label_material = new THREE.MeshBasicMaterial({
             map: texture,
             side: THREE.DoubleSide,
             transparent: true,
             opacity: 1,
             //depthWrite: false,
          });
          const geometry = new THREE.PlaneGeometry(canvas.width, canvas.height);
          const label = new THREE.Mesh(geometry, label_material);
          label.scale.set(scale, scale, 1);
          label.position.set(location.x + scale*LABEL_WIDTH/2, location.y - scale*LABEL_HEIGHT/2 , location.z);
          
          return label;
       }

       function render() {
          renderer.render(scene, camera);
          window.requestAnimationFrame( () => this.render() );
          this.camControls.update();
       }

       function makeSphere(location, radius, properties = {color: '#000000'}) {
          const material = new THREE.MeshPhongMaterial(properties);
          const geometry = new THREE.SphereGeometry(radius, 10, 10);
          const sphere = new THREE.Mesh(geometry, material);
          sphere.position.set(...location.toArray());

          return sphere;
       }

       function y(x, y0) {
          return -y0*(x-1)*(x+1);
       }

       function makeCurve([start, middle, end], color = 'black', dashed = false) {
          const middle_control = new THREE.Vector3(middle.x, 2*middle.y, middle.z),
                curve = new THREE.QuadraticBezierCurve3(start, middle_control, end),
                vertices = curve.getPoints(20),
                properties = {
                   color: color,
                   lineWidth: 5,
                   sizeAttenuation: false,
                   side: THREE.DoubleSide,
                   resolution: new THREE.Vector2(((window.innerWidth /*: any */) /*: float */),
                                                 ((window.innerHeight /*: any */) /*: float */)),
                };
          if (dashed) {
             properties.dashArray = 0.03;
             properties.dashOffset = 0;
             properties.dashRatio = 0.5;
             properties.opacity = 1;
             properties.transparent = true;
          }
          const material = new MeshLineMaterial(properties);
          const geometry = new THREE.Geometry();
          geometry.vertices = vertices;
          const mesh_line = new MeshLine();
          mesh_line.setGeometry(geometry);
          const curved_line = new THREE.Mesh(mesh_line.geometry, material);

          return curved_line;
       }

       function makeLine(vertices, color = 'black') {
          const geometry = new THREE.Geometry();
          geometry.vertices = vertices;
          const lineMaterial = new THREE.LineBasicMaterial({color: color});
          const line = new THREE.Line(geometry, lineMaterial);

          return line;
       }

       function makeMeshLine(vertices, color = 'black', dashed = false, lineWidth = 10) {
          const material_properties = {
             color: new THREE.Color(color),
             lineWidth: lineWidth,
             sizeAttenuation: false,
             side: THREE.DoubleSide,
             resolution: new THREE.Vector2(((window.innerWidth /*: any */) /*: float */),
                                           ((window.innerHeight /*: any */) /*: float */)),
          };
          if (dashed) {
             material_properties.dashArray = 0.03;
             material_properties.dashOffset = 0;
             material_properties.dashRatio = 0.5;
             material_properties.opacity = 1;
             material_properties.transparent = true;
          };
          const material = new MeshLineMaterial(material_properties);
          const geometry = new THREE.Geometry();
          geometry.vertices = vertices;
          const mesh_line = new MeshLine();
          mesh_line.setGeometry(geometry);
          const line = new THREE.Mesh(mesh_line.geometry, material);

          return line;
       }

       function makeArrowhead(vertices, color = 'black') {
          const l = vertices[1].clone().sub(vertices[0]),
                ll = l.length(),
                t = 1 - 0.21 / ll,
                origin = vertices[0].clone().multiplyScalar(1-t).addScaledVector(vertices[1], t),
                line = vertices[1].clone().sub(origin),
                length = line.length(),
                dir = line.normalize(),
                head_length = Math.min(0.13, 0.3*new THREE.Line3(vertices[0], vertices[1]).distance()),
                arrowhead = new THREE.ArrowHelper(dir, origin, length, color, head_length, 0.3*head_length);

          return arrowhead;
       }

       function makeLabel(location, text, x_offset = 0, color = 'black', label_font) {
          const canvas = document.createElement('canvas'),
                context = canvas.getContext('2d'),
                text_width = context.measureText(text).width,
                short_text = (text_width < 150),
                label_width = short_text ? 512 : 2048,
                label_height = 64,
                scale = short_text ? 1 : 4;
          canvas.width = label_width;
          canvas.height = label_height;
          context.font = `${label_font}pt Arial`;
          //context.fillStyle = 'rgba(0, 0, 100, 0.5)';
          //context.fillRect(0, 0, canvas.width, canvas.height);
          context.fillStyle = color;
          context.fillText(text, 0, 0.7*label_height);
          //console.log(`${text} -- ${text_width} -- ${label_width}`);

          const texture = new THREE.CanvasTexture(canvas),
                labelMaterial = new THREE.SpriteMaterial({
                   map: texture,
                   side: THREE.DoubleSide,
                   transparent: true,
                   opacity: 1,
                   depthWrite: false,
                }),
                label = new THREE.Sprite(labelMaterial);
          label.scale.set(scale, scale*canvas.height/canvas.width, 1.0);
          label.center = new THREE.Vector2(x_offset/scale, 0.5);
          label.position.set(...location.toArray());
          
          return label;
       }

       function makePlane(position) {
          const geometry = new THREE.PlaneGeometry(3.1, 2.1);
          material = new THREE.MeshBasicMaterial({
             color: '#E8C8C8',
             opacity: 0.4,
             transparent: true,
             depthWrite: false,
             //depthTest: false,
          }),
                plane = new THREE.Mesh(geometry, material);
          plane.position.set(...position.toArray());

          return plane;
       }

       function showAxes() {
          scene.add(makeLine([new THREE.Vector3(0,0,0), new THREE.Vector3(2,0,0)], 'red'));
          scene.add(makeLine([new THREE.Vector3(0,0,0), new THREE.Vector3(0,2,0)], 'green'));
          scene.add(makeLine([new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,2)], 'blue'));
       }
      </script>
   </head>
   <body>
      <div id="graphic"></div>
   </body>
</html>
